package org.web.clientlistmanager.client.biz;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import org.web.clientlistmanager.client.config.ClmClientConfig;
import org.web.clientlistmanager.client.domain.AccessServerDTO;
import org.web.clientlistmanager.sdk.event.EventHelper;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;


@Setter
@Getter
@Component
public class ClmClientMonitor extends Thread {
    private static final Logger logger = LogManager.getLogger(ClmClientMonitor.class);

    /**
     * 是否需要重新创建连接标识位，默认值是true，即需要重新创建连接。
     * 变更的场合有：
     * （1）第一次创建连接成功，则重置needConnectMark = false。定时任务如果发现needConnectMark = true，则会尝试创建新的连接。
     * （2）执行closeChannel（）方法释放资源时，会设置needConnectMark = true。
     * （3）基于第二点，何时会执行closeChannel,
     * (3-1)有两个场合，定时任务检查Channel 不是active，则设置needConnectMark = true。
     * (3-2)发送心跳异常，出现异常，则设置needConnectMark = true。
     * */
    private static volatile boolean needConnectMark = true;

    /***
     * 是否已经注册标识位，客户端发送注册信息，并不能保证服务端接收到，以及服务端接收到是否处理成功，需要得到服务端的响应回执，才能变更其状态。
     * 所以注册流程是：
     * 第一步，客户端发送注册信息
     * 第二步，服务端发送注册成功与否ACK，在连接正常的场合下，客户端没有收到ACK的场合下，定时轮询时都会尝试注册自己。（PS：服务端已经做好幂等处理）
     * 第三步，客户端收到ACK，根据ACK的状态变更自己的状态。如果告知返回成功，则设置hasReportSelf = true。下次定时轮询时，则不再进行测试册自己。
     */
    public static volatile boolean hasReportSelf = false;

    private static final int CONNECTION_TIMEOUT_SECONDS = 10;
    private static final long POLL_INTERVAL = 2000L;
    private static final long SLEEP_INTERVAL = 1000L;

    /***
     * 服务端回执注册成功的信息，是服务端的唯一标识
     */
    private String  serverPartProcessByReportSelfAck = null;

    /**
     * random 操作accessServerArrayList数组下标，随机获取信息。
     */
    private Random random = new Random();

    private ClmClientConfig clmClientConfig;


    /**
     * Set中有值，说明上一次轮休是失败的。
     * 设计的目的：
     * （1）是加快处理流程。第一次创建连接失败的时候，不sleep，直接执行下次。
     * （2）连续创建连接失败，进行sleep.
     * (3) 连接创建成功时，从Set中剔除连接。
     */
    private List<AccessServerDTO> accessServerArrayList;

    private final EventLoopGroup loopGroup;
    private final Bootstrap bootstrap;

    private Channel channel;

    @Resource
    private ClmNettyClientHandler clmNettyClientHandler;

    public ClmClientMonitor(List<AccessServerDTO> accessServerArrayList) {
        this.accessServerArrayList = accessServerArrayList;
        this.loopGroup = new NioEventLoopGroup();
        this.bootstrap = new Bootstrap().group(loopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) {
                socketChannel.pipeline().addLast(clmNettyClientHandler);
            }
        });
    }


    /***
     * 监控事件：
     * (1) 重建连接：
     *   优先级别：第一高。
     *   功能描述：根据 needConnectMark 判断是否需要重新创建连接, needConnectMark = true, 表示需要重新创建连接，创建连接成功，则重置成员变更Channel.
     *   服务器启动时，needConnectMark默认值是true。
     *（2）channel 状态检查
     *   优先级别：第二高。
     *   功能描述：channel 如果 not active, 则设置 needConnectMark = false, 等待下次执行时，尝试构建新的连接。对于老的channel 显示的时行close,释放资源。
     *（3）客户端向服务端发产送心跳
     *   优先级别：通过逻辑，可以识别出执行心跳操作，是基于前面所监控资源都正常的情况。
     *   功能描述：发送心跳。
     *   另外的方案：
     * */
    @SneakyThrows
    @Override
    public void run() {
        for (; ; ) {
            try {
                // 不要调整if\else if 的顺序。
                if (needConnectMark) {
                    List<AccessServerDTO> copyList = buildDuplicateAddressList(accessServerArrayList);
                    while (!copyList.isEmpty()) {
                        AccessServerDTO accessServerDTO = getRandomAddress(copyList);
                        // Create a new connection
                        ChannelFuture future = bootstrap.connect(accessServerDTO.getIp(), accessServerDTO.getPort());
                        // Wait for the connection to be established with a timeout
                        if (future.await(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                            if (future.isSuccess()) {
                                channel = future.channel();
                                if (channel.isActive()) {
                                    // 不要放在下面，原因是已经成功连接，需要设置needConnectMark = false，等待下次执行时，不再尝试构建新的连接。有可能下面会出现异常，但是不需要重新创建连接。
                                    needConnectMark = false;
                                    EventHelper.sendClmRequest(channel, clmClientConfig.getReportSelfEventRequest());
                                    // 需要等待注册成功的回执，才可以设置hasReportSelf = true
                                    break; // 别看错了，跳出的是
                                }
                                // Connection established successfully, you can now send/receive data
                            } else {
                                // Connection failed, handle the error and requeue the IP
                                logger.error(future.cause().getMessage());
                                logger.error("Access IP is " + accessServerDTO.getIp() + " , port is " + accessServerDTO.getPort() + ", 连接失败，请检查服务端服务是否存在。 ");
                            }
                        } else {
                            // Connection attempt timed out
                            logger.error("Connection timed out， access ip is " + accessServerDTO);
                        }
                        copyList.remove(accessServerDTO);
                    }
                    if (needConnectMark) {
                        logger.error("当前没有任何可用的服务端！！！");
                    }
                } else if (channel == null || !channel.isActive()) {
                    closeChannel(channel);
                } else if (!hasReportSelf) {
                    EventHelper.sendClmRequest(channel, clmClientConfig.getReportSelfEventRequest());
                    // 等待注册成功的回执hasReportSelf = true;
                } else {
                    // 示例：在客户端发送消息后检查发送是否成功
                    // （3）客户端向服务端发产送心跳,发送唯一标识。
                    ChannelFuture sendHeartBeatFuture = EventHelper.sendClmRequest(channel, clmClientConfig.getHeartBeatEventRequest());
                     //每次writeAndFlush都会创建一个新的ChannelFuture对象,addListener是针对这个具体的ChannelFuture对象的，而不是全局的。
                     //这意味着你通过future2.addListener添加的监听器只会监听与future2相关联的那次writeAndFlush操作的完成状态。
                    sendHeartBeatFuture.addListener((ChannelFutureListener) futureListener -> {
                        if (!futureListener.isSuccess()) {
                            sendHeartBeatFuture.channel().close();
                            closeChannel(channel);
                        }
                    });
                }
                Thread.sleep(SLEEP_INTERVAL);
            } catch (InterruptedException e) {
                logger.error("Thread interrupted: " + e.getMessage());
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                logger.error("Unexpected error: " + e.getMessage());
            } finally {
                Thread.sleep(5000L);
            }
        }
    }

    // 新增的方法，用于重新构建 addresses 列表
    public List<AccessServerDTO> buildDuplicateAddressList(List<AccessServerDTO> accessServerArrayList) {
        return new ArrayList<>(accessServerArrayList);
    }

    private AccessServerDTO getRandomAddress(List<AccessServerDTO> accessServerArrayList) {
        int randomIndex = random.nextInt(accessServerArrayList.size());
        return accessServerArrayList.get(randomIndex);
    }

    /**
     * 手动强制close
     */
    private void closeChannel(Channel channel) {
        try {
            if (channel != null) {
                channel.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        } finally {
            needConnectMark = true;
        }
    }

    public void shutdown() {
        loopGroup.shutdownGracefully();
    }
}

